Remove explicit useMemo (React Compiler handles it)#150
Remove explicit useMemo (React Compiler handles it)#150
Conversation
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
1557522 to
015af9b
Compare
Code Review: Remove explicit useMemo (React Compiler handles it)Critical Issue: IIFE Does Not Provide Memoization ❌The core change in this PR introduces a significant bug. The React Compiler does not automatically memoize IIFEs (Immediately Invoked Function Expressions) the same way it would memoize a The ProblemLines 19-32 replace // Before (correct):
const pageIdPromise = useMemo(() => {
// ... Promise-returning logic
}, [wikiUrl, debouncedPageTitle]);
// After (buggy):
const pageIdPromise = (async () => {
// ... async logic
})();This IIFE will execute on EVERY render, not just when
Test Coverage IssueThe new test (lines 223-245) claims to verify memoization behavior, but it may not be catching this bug because:
React Compiler LimitationsReact Compiler can optimize many patterns, but it cannot automatically memoize IIFEs because:
Recommendations
What React Compiler Actually MemoizesReact Compiler automatically memoizes:
It does not replace the need for
Architecture ConsiderationThe original
Verdict: This PR should not be merged as-is. The removal of |
Response to ReviewThe review's concern about memoization has been investigated and the test evidence contradicts the claims. Test AddedA memoization verification test was added in commit 015af9b:
Result: Test Passes ✅CI Run #21169110004 shows all 68 tests pass. ConclusionReact Compiler ( The review's claim that "React Compiler cannot automatically memoize side effects" is incorrect for this pattern - the compiler analyzes the dependency graph and memoizes the Promise creation based on |
|
@claude revise it |
|
Claude finished @Wintus's task in 1m 4s —— View job Revised Code Review
Conclusion: My Previous Reviews Were Incorrect ✅After carefully examining the code, test, and CI results, I must revise my assessment. You were correct, and my previous reviews were wrong. Evidence Analysis1. The Test Is Well-Designed (lines 223-245) it('does not call fetchPageId on re-render when debouncedPageTitle has not changed', async () => {
// Initial render: fetchPageId called once for "Test"
expect(mockFetchPageId).toHaveBeenCalledTimes(1);
// Type 's' -> triggers setPageTitle and re-render
await user.type(inputElement, 's');
// CRITICAL: debouncedPageTitle is still "Test" (300ms debounce hasn't completed)
// If IIFE runs on every render, fetchPageId would be called again
// But test expects it NOT to be called:
expect(mockFetchPageId).toHaveBeenCalledTimes(1); // Still 1, not 2
});This test specifically validates that the Promise is not recreated when dependencies don't change. 2. Test Passes in CI ✅
3. React Compiler Is Configured ✅ react({
babel: {
plugins: ['babel-plugin-react-compiler'],
},
})Why React Compiler Can Memoize This PatternThe React Compiler (
The compiler doesn't just optimize JSX - it can memoize any expression based on its reactive dependencies. What I Got WrongMy previous reviews incorrectly claimed:
I made assumptions about React Compiler's limitations without verifying against actual behavior. The test evidence clearly shows memoization works. Code Quality NotesMinor Issue (line 27): return ({ id: id.toString() });Unnecessary parentheses around the return object. Should be: return { id: id.toString() };This is a style issue, not a functional bug. Recommendation✅ Approve this PR. The changes are correct:
ApologyI apologize for the incorrect reviews. I should have:
Thank you for pushing back and asking me to revise. The test you added is excellent and proves the implementation is correct. |
015af9b to
9b32ef9
Compare
This comment was marked as outdated.
This comment was marked as outdated.
262d4bb to
74ca2b8
Compare
PR Review: Remove explicit useMemo (React Compiler handles it)SummaryThis PR removes explicit Critical Issues 🚨1. IIFE Does Not Memoize - This Breaks the ComponentThe current implementation using an IIFE (Immediately Invoked Function Expression) will recreate the promise on every render, causing the exact problem // Current code - runs on EVERY render
const pageIdPromise = (async () => {
// ...
})();Problem: Even though React Compiler can memoize values, an IIFE is executed immediately during render, not assigned as a stable reference that can be memoized. Each render creates a new promise instance. Evidence: The new test added in this PR (lines 223-245) expects
Impact:
2. Inconsistent Return ValueLine 22 has if (!debouncedPageTitle) {
return {}; // Not Promise.resolve({})
}Since we're in an RecommendationsOption A: Keep
|
This comment was marked as outdated.
This comment was marked as outdated.
74ca2b8 to
09948b8
Compare
Code Review Feedback🚨 Critical Issue: IIFE Does Not Provide MemoizationThe refactoring from Problem: // Current code (BROKEN)
const pageIdPromise = (async () => {
if (!debouncedPageTitle) return {};
try {
const id = await fetchPageId(wikiUrl, debouncedPageTitle);
return { id: id.toString() };
} catch (error) {
return { error: '...' };
}
})(); // ← This executes IMMEDIATELY on every renderWhat happens:
Why the test passes (false positive): The test at line 223-243 passes because:
The original
📋 Recommended FixYou have two options: Option 1: Keep const pageIdPromise = useMemo(() => {
if (!debouncedPageTitle) {
return Promise.resolve({});
}
return fetchPageId(wikiUrl, debouncedPageTitle)
.then((id) => ({ id: id.toString() }))
.catch((error) => {
console.error('Error fetching page ID:', error);
return { error: 'Error fetching page ID. Please try again.' };
});
}, [wikiUrl, debouncedPageTitle]);React Compiler is smart enough to recognize when Option 2: Use import { use, useMemo } from 'react';
const pageIdPromise = useMemo(
() => fetchPageIdWithGuard(wikiUrl, debouncedPageTitle),
[wikiUrl, debouncedPageTitle]
);
// Later in render:
const result = use(pageIdPromise);But this requires restructuring the component significantly. ✅ What's Good
🔍 Additional Observations
it('does not call fetchPageId on re-render when debouncedPageTitle has not changed', async () => {
// ... setup ...
await user.type(inputElement, 's');
// Wait for debounce to complete
await new Promise(resolve => setTimeout(resolve, 350));
// Should still only have 1 call since debouncedPageTitle hasn't changed yet
expect(mockFetchPageId).toHaveBeenCalledTimes(1);
});
📚 References
RecommendationDo not merge this PR as-is. Revert to |
Why This Is Not a "False Positive"The test correctly validates memoization: What the test does:
If IIAFE ran every render (as claimed):
Test passes → IIAFE is NOT re-executedThe suggestion to wait 350ms tests something different: whether a new debounced value triggers a fetch. That's expected behavior—not what we're validating. We're testing that re-renders with unchanged dependencies don't re-execute the IIAFE. CI EvidenceCI Run #21174990839 - 68 tests pass. changelogs
|
Broken link. Here is the correct one: https://react.dev/learn/react-compiler/introduction#what-should-i-do-about-usememo-usecallback-and-reactmemo
|
|
@claude do you note it? |
|
Claude finished @Wintus's task in 1m 25s —— View job Final Assessment
Yes, I note it. And I need to correct my previous reviews.After thorough analysis of the code, test, React Compiler documentation, and CI results, here's the definitive answer: The Core Question: Does React Compiler Memoize IIFEs?The answer is nuanced and requires understanding what React Compiler actually does. React Compiler works by analyzing component code and automatically inserting memoization boundaries. When it compiles your component, it:
What Your Test Actually ProvesYour test at lines 223-243 is well-designed and tests the right thing: // Initial render with "Test" → fetchPageId called once
expect(mockFetchPageId).toHaveBeenCalledTimes(1);
// Type 's' → triggers setPageTitle("Tests") → component re-renders
await user.type(inputElement, 's');
// debouncedPageTitle is STILL "Test" (300ms debounce hasn't fired)
// If pageIdPromise IIFE re-executed, fetchPageId would be called again
expect(mockFetchPageId).toHaveBeenCalledTimes(1); // ✅ Still 1Test passes → Proves the IIFE is NOT re-executing when dependencies ( How This Works with React CompilerWhen React Compiler processes this component, it sees: const pageIdPromise = (async () => {
if (!debouncedPageTitle) return {};
const id = await fetchPageId(wikiUrl, debouncedPageTitle);
return { id: id.toString() };
})();The compiler analyzes that const pageIdPromise = useMemo(() => (async () => {
// ... same logic
})(), [wikiUrl, debouncedPageTitle]);Evidence Supporting This
My Previous Reviews Were WrongI apologize for the confusion in my previous reviews. I made incorrect assumptions about:
The empirical evidence (passing tests, successful CI) contradicts my theoretical concerns. However: A Critical ClarificationWhile React Compiler can memoize IIFEs in practice (as your test proves), this is not officially documented behavior. The React Compiler docs focus on:
They explicitly state that
My RecommendationGiven the evidence: ✅ The PR is technically correct - React Compiler does memoize this IIFE pattern, as proven by the test. However, I would suggest considering:
The React Compiler docs recommend for existing code: "Leave existing memoization in place (removing it can change compilation output)." Why This MattersThe Bottom line: Your test proves the behavior works correctly. The PR can be merged if you're comfortable relying on React Compiler's current implementation behavior. My previous "critical issue" assessments were incorrect based on the empirical evidence. |
|
JFYI, the inner Wikipedia-Blame/src/components/PageIdFetcher.tsx Lines 8 to 9 in 09948b8 |
React Compiler auto-memoizes based on reactive dependencies. Co-Authored-By: Claude
09948b8 to
c725e8f
Compare
Code Review: Remove explicit useMemo (React Compiler handles it)SummaryThis PR removes 🚨 Critical BugIssue: Promise is recreated on every renderLines 19-32: The current implementation creates a new Promise on every render: const pageIdPromise = (async () => {
// ... async logic
})();This is an Immediately Invoked Function Expression (IIFE) that executes on every render, meaning:
Why React Compiler Cannot Fix ThisReact Compiler can auto-memoize values and callbacks, but it cannot prevent an IIFE from executing on every render. The The Root ProblemThe original code used
Removing 📋 Required FixYou need to either: Option A: Keep useMemo (Recommended)Revert this change. The const pageIdPromise = useMemo(() => {
return (async () => {
if (!debouncedPageTitle) return {};
try {
const id = await fetchPageId(wikiUrl, debouncedPageTitle);
return { id: id.toString() };
} catch (error) {
console.error('Error fetching page ID:', error);
return { error: 'Error fetching page ID. Please try again.' };
}
})();
}, [wikiUrl, debouncedPageTitle]);Option B: Use a different patternIf you want to avoid 🧪 Why Tests Still Pass (False Positive)The tests pass because:
To catch this bug, you'd need a test that: it('does not recreate pageIdPromise on unrelated re-renders', async () => {
const { rerender } = render(<PageTitleInput initialPageTitle="Test" wikiUrl={url} />);
const callCountBefore = mockFetchPageId.mock.calls.length;
// Force a re-render without changing dependencies
rerender(<PageTitleInput initialPageTitle="Test" wikiUrl={url} />);
// Should not trigger new API call
expect(mockFetchPageId.mock.calls.length).toBe(callCountBefore);
});📊 Code Quality AssessmentPositive Changes
Issues
🎯 Recommendations
📚 Additional ContextReact Compiler excels at auto-memoizing:
But it cannot prevent:
This is a case where explicit Security & Performance
Verdict: ❌ Request Changes - Critical bug must be fixed before merge. Co-Authored-By: Claude Sonnet 4.5 noreply@anthropic.com |
|
Closing in favor of #155. RationaleWhile this PR removes explicit Issue #155 proposes a more comprehensive solution: migrating to TanStack Query's
The React Compiler optimization from this PR would be superseded by the TanStack Query migration. Co-Authored-By: Claude |
Summary
useMemofromPageTitleInputcomponentVerification
References
Co-Authored-By: Claude